#include "D3DFramework.h"

#include <sstream>

#include "D3DErrorChecker.h"

namespace D3D10
{
    LRESULT CALLBACK
    MainWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {
    	static D3DFramework *application = nullptr;

    	switch(msg)
    	{
    		case WM_CREATE:
    		{
    			// Get the 'this' pointer we passed to CreateWindow via the lpParam parameter.
    			CREATESTRUCT *createStruct = reinterpret_cast<CREATESTRUCT*> (lParam);
    			application = reinterpret_cast<D3DFramework*> (createStruct->lpCreateParams);
    			
    			return 0;
    		}
    	}

    	// Don't start processing messages until after WM_CREATE.
    	return (application) ? application->msgProc(msg, wParam, lParam) : DefWindowProc(hwnd, msg, wParam, lParam);
    }

    D3DFramework::D3DFramework(const HINSTANCE handleInstance)
        : mFrameStats(L"")
        , mMainWindowCaption(L"D3D10 Application")
		, mDXGIFactory(nullptr)
        , mDevice(nullptr)
    	, mSwapChain(nullptr)
    	, mDepthStencilBuffer(nullptr)
    	, mRenderTargetView(nullptr)
    	, mDepthStencilView(nullptr)
    	, mDriverType(D3D10_DRIVER_TYPE_HARDWARE)
        , mClearColor(1.0f, 1.0f, 1.0f, 1.0f)
    	, mAppInstance(handleInstance)
    	, mMainWindow(nullptr)
    	, mClientWidth(800)
    	, mClientHeight(600)
        , mElapsedFrames(0)
        , mElapsedTimeInSeconds(0.0f)
    	, mIsPaused(false)
    	, mIsMinimized(false)
    	, mIsMaximized(false)
    	, mResizing(false)
    {	
    }

    D3DFramework::~D3DFramework()
    {
		mDXGIFactory->Release();
		mRenderTargetView->Release();
    	mDepthStencilView->Release();
    	mSwapChain->Release();
    	mDepthStencilBuffer->Release();
    	mDevice->Release();
    }

    int D3DFramework::run()
    {
    	MSG msg = {0};
     
    	mTimer.reset();

    	while(msg.message != WM_QUIT)
    	{
    		// If there are Window messages then process them.
    		if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
    		{
                TranslateMessage(&msg);
                DispatchMessage(&msg);
    		}
    		
    		// Otherwise, do animation/game stuff.
    		else
            {	
    			mTimer.tick();

    			(!mIsPaused) ? updateScene(mTimer.getDeltaTime()) : Sleep(50);

    			drawScene();
            }
        }
        
    	return static_cast<int> (msg.wParam);
    }

    void D3DFramework::init()
    {
    	initMainWindow();
    	initDirect3D();
    }
     
    void D3DFramework::onResize()
    {
    	// Release the old views, as they hold references to the buffers we
    	// will be destroying.  Also release the old depth/stencil buffer.
    	if (mRenderTargetView)
        {
            mRenderTargetView->Release();
            mRenderTargetView = 0;
        }

        if (mDepthStencilView)
        {
            mDepthStencilView->Release();
            mDepthStencilView = 0;
        }

        if (mDepthStencilBuffer)
        {
            mDepthStencilBuffer->Release();
            mDepthStencilBuffer = 0;
        }

    	// Resize the swap chain and recreate the render target view.
		HRESULT hr = mSwapChain->ResizeBuffers(1, mClientWidth, mClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0);
		D3D10Utils::ErrorChecker(hr);
    	ID3D10Texture2D *backBuffer = 0;
		hr = mSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast<void**> (&backBuffer));
    	D3D10Utils::ErrorChecker(hr);

		hr = mDevice->CreateRenderTargetView(backBuffer, 0, &mRenderTargetView);
    	D3D10Utils::ErrorChecker(hr);
    	
    	backBuffer->Release();

    	// Create the depth/stencil buffer and view.
    	D3D10_TEXTURE2D_DESC depthStencilDesc;	
    	depthStencilDesc.Width = mClientWidth;
    	depthStencilDesc.Height = mClientHeight;
    	depthStencilDesc.MipLevels = 1;
    	depthStencilDesc.ArraySize = 1;
    	depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    	depthStencilDesc.SampleDesc.Count = 1; // multisampling must match
    	depthStencilDesc.SampleDesc.Quality = 0; // swap chain values.
    	depthStencilDesc.Usage = D3D10_USAGE_DEFAULT;
    	depthStencilDesc.BindFlags = D3D10_BIND_DEPTH_STENCIL;
    	depthStencilDesc.CPUAccessFlags = 0; 
    	depthStencilDesc.MiscFlags = 0;

		hr = mDevice->CreateTexture2D(&depthStencilDesc, 0, &mDepthStencilBuffer);
    	D3D10Utils::ErrorChecker(hr);
		hr = mDevice->CreateDepthStencilView(mDepthStencilBuffer, 0, &mDepthStencilView);
    	D3D10Utils::ErrorChecker(hr);

    	// Bind the render target view and depth/stencil view to the pipeline.
    	mDevice->OMSetRenderTargets(1, &mRenderTargetView, mDepthStencilView);
    	
    	// Set the viewport transform.
    	D3D10_VIEWPORT viewPort;
    	viewPort.TopLeftX = 0;
    	viewPort.TopLeftY = 0;
    	viewPort.Width = mClientWidth;
    	viewPort.Height = mClientHeight;
    	viewPort.MinDepth = 0.0f;
    	viewPort.MaxDepth = 1.0f;

    	mDevice->RSSetViewports(1, &viewPort);
    }

    void D3DFramework::updateScene(const float deltaTime)
    {
        ++mElapsedFrames;

       	// Compute averages over one second period.
    	if ((mTimer.getInGameTime() - mElapsedTimeInSeconds) >= 1.0f)
    	{
    		const float fps = static_cast<float> (mElapsedFrames); // fps = mNumFrames / 1
    		const float millisecondsPerFrame = 1000.0f / fps; 

    		std::wostringstream outs;   
    		outs.precision(6);
    		outs << L"FPS: " << fps << L"\n    " 
    			 << "Milliseconds per frame: " << millisecondsPerFrame;
    		mFrameStats = outs.str();
    		
            SetWindowText(mMainWindow, mFrameStats.c_str());

    		// Reset for next average.
    		mElapsedFrames = 0;
    		mElapsedTimeInSeconds += 1.0f;
    	}
    }

    void D3DFramework::drawScene()
    {
    	mDevice->ClearRenderTargetView(mRenderTargetView, reinterpret_cast<float*> (&mClearColor));
    	mDevice->ClearDepthStencilView(mDepthStencilView, D3D10_CLEAR_DEPTH | D3D10_CLEAR_STENCIL, 1.0f, 0);
    }

    LRESULT D3DFramework::msgProc(UINT msg, WPARAM wParam, LPARAM lParam)
    {
    	switch(msg)
    	{
    	// WM_ACTIVATE is sent when the window is activated or deactivated.  
    	// We pause the game when the window is deactivated and unpause it 
    	// when it becomes active.  
    	case WM_ACTIVATE:
    		if (LOWORD(wParam) == WA_INACTIVE)
    		{
    			mIsPaused = true;
    			mTimer.stop();
    		}
    		else
    		{
    			mIsPaused = false;
    			mTimer.start();
    		}
    		
    		return 0;

    	// WM_SIZE is sent when the user resizes the window.  
    	case WM_SIZE:
    		// Save the new client area dimensions.
    		mClientWidth = LOWORD(lParam);
    		mClientHeight = HIWORD(lParam);
    		if (mDevice)
    		{
    			if (wParam == SIZE_MINIMIZED)
    			{
    				mIsPaused = true;
    				mIsMinimized = true;
    				mIsMaximized = false;
    			}
    			
    			else if (wParam == SIZE_MAXIMIZED)
    			{
    				mIsPaused = false;
    				mIsMinimized = false;
    				mIsMaximized = true;
    				onResize();
    			}
    			else if (wParam == SIZE_RESTORED)
    			{				
    				// Restoring from minimized state?
    				if (mIsMinimized)
    				{
    					mIsPaused = false;
    					mIsMinimized = false;
    					onResize();
    				}

    				// Restoring from maximized state?
    				else if (mIsMaximized)
    				{
    					mIsPaused = false;
    					mIsMaximized = false;
    					onResize();
    				}
    				
    				else if (mResizing)
    				{
    					// If user is dragging the resize bars, we do not resize 
    					// the buffers here because as the user continuously 
    					// drags the resize bars, a stream of WM_SIZE messages are
    					// sent to the window, and it would be pointless (and slow)
    					// to resize for each WM_SIZE message received from dragging
    					// the resize bars.  So instead, we reset after the user is 
    					// done resizing the window and releases the resize bars, which 
    					// sends a WM_EXITSIZEMOVE message.
    				}
    				else // API call such as SetWindowPos or mSwapChain->SetFullscreenState.
    				{
    					onResize();
    				}
    			}
    		}
    		
    		return 0;

    	// WM_EXITSIZEMOVE is sent when the user grabs the resize bars.
    	case WM_ENTERSIZEMOVE:
    		mIsPaused = true;
    		mResizing = true;
    		mTimer.stop();
    		
    		return 0;

    	// WM_EXITSIZEMOVE is sent when the user releases the resize bars.
    	// Here we reset everything based on the new window dimensions.
    	case WM_EXITSIZEMOVE:
    		mIsPaused = false;
    		mResizing = false;
    		mTimer.start();
    		onResize();
    		
    		return 0;
     
    	// WM_DESTROY is sent when the window is being destroyed.
    	case WM_DESTROY:
    		PostQuitMessage(0);
    		
    		return 0;

    	// The WM_MENUCHAR message is sent when a menu is active and the user presses 
    	// a key that does not correspond to any mnemonic or accelerator key. 
    	case WM_MENUCHAR:
            
            // Don't beep when we alt-enter.
            return MAKELRESULT(0, MNC_CLOSE);

    	// Catch this message so to prevent the window from becoming too small.
    	case WM_GETMINMAXINFO:
    	    {
        	    MINMAXINFO * const minMaxInfo = reinterpret_cast<MINMAXINFO * const> (lParam);
        		minMaxInfo->ptMinTrackSize.x = 200;
        		minMaxInfo->ptMinTrackSize.y = 200; 

        		return 0;
    	    }   
    	}

    	return DefWindowProc(mMainWindow, msg, wParam, lParam);
    }


    void D3DFramework::initMainWindow()
    {
    	WNDCLASS wndClass;
    	wndClass.style = CS_HREDRAW | CS_VREDRAW;
    	wndClass.lpfnWndProc = MainWindowProc; 
    	wndClass.cbClsExtra = 0;
    	wndClass.cbWndExtra = 0;
    	wndClass.hInstance = mAppInstance;
    	wndClass.hIcon = LoadIcon(0, IDI_APPLICATION);
    	wndClass.hCursor = LoadCursor(0, IDC_ARROW);
    	wndClass.hbrBackground = static_cast<HBRUSH> (GetStockObject(NULL_BRUSH));
    	wndClass.lpszMenuName = 0;
    	wndClass.lpszClassName = L"D3DWndClassName";

    	if(!RegisterClass(&wndClass))
    	{
    		MessageBox(0, L"RegisterClass FAILED", 0, 0);
    		PostQuitMessage(0);
    	}

    	// Compute window rectangle dimensions based on requested client area dimensions.
    	RECT R = { 0, 0, mClientWidth, mClientHeight };
        AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
    	const int width  = R.right - R.left;
    	const int height = R.bottom - R.top;

    	mMainWindow = CreateWindow(L"D3DWndClassName", mMainWindowCaption.c_str(), 
    		WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mAppInstance, this); 
    	if (!mMainWindow)
    	{
    		MessageBox(0, L"CreateWindow FAILED", 0, 0);
    		PostQuitMessage(0);
    	}

        ShowWindow(mMainWindow, SW_SHOW);
    	UpdateWindow(mMainWindow);
    }

    void D3DFramework::initDirect3D()
    {
    	// Fill out a DXGI_SWAP_CHAIN_DESC to describe our swap chain.
    	DXGI_SWAP_CHAIN_DESC swapChainDesc;
    	swapChainDesc.BufferDesc.Width = mClientWidth;
    	swapChainDesc.BufferDesc.Height = mClientHeight;
    	swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
    	swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
    	swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    	swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    	swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    	swapChainDesc.SampleDesc.Count = 1;
    	swapChainDesc.SampleDesc.Quality = 0;
    	swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    	swapChainDesc.BufferCount = 1;
    	swapChainDesc.OutputWindow = mMainWindow;
    	swapChainDesc.Windowed = true;
    	swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    	swapChainDesc.Flags = 0;

    	// Create the device.
    	UINT createDeviceFlags = 0;
    #if defined(DEBUG) || defined(_DEBUG)  
        createDeviceFlags |= D3D10_CREATE_DEVICE_DEBUG;
    #endif

		// Create device
		HRESULT hr = D3D10CreateDevice(0, mDriverType, 0, createDeviceFlags, D3D10_SDK_VERSION, &mDevice);
		D3D10Utils::ErrorChecker(hr);
    	
		// An IDXGIFactory interface implements methods for generating DXGI objects 
		// (which handle full screen transitions).
		hr = CreateDXGIFactory(__uuidof(IDXGIFactory), reinterpret_cast<void**> (&mDXGIFactory));
		D3D10Utils::ErrorChecker(hr);
		
		hr = mDXGIFactory->CreateSwapChain(mDevice, &swapChainDesc, &mSwapChain);
		D3D10Utils::ErrorChecker(hr);

    	// The remaining steps that need to be carried out for d3d creation
    	// also need to be executed every time the window is resized.  So
    	// just call the onResize method here to avoid code duplication.
    	onResize();
    }
}
